Transit Priority

import pandas as pd
import matplotlib.pyplot as plt
import geopandas as gpd
import shapely
import folium
from folium import Choropleth

Adding Vulnerability to Bus Metrics Analysis

To evaluate the relationship between transit service provision and community needs, the Transit Vulnerability Index (TVI) was spatially joined with bus network data aggregated by census tracts. This integration enabled a deeper analysis of how key transit metrics—such as frequency, headway, and ridership—align with areas of heightened social vulnerability.

#Import Data:
peak_metrics_network = gpd.read_file("Data/peak_metrics_network.geojson")
vulnerability = gpd.read_file("Data/vulnerability.geojson")
bus_network_philadelphia = gpd.read_file("Data/bus_network_philadelphia.geojson")
peak_metrics_network = peak_metrics_network.to_crs("EPSG: 2272")
vulnerability = vulnerability.to_crs("EPSG: 2272")

# Rename columns in the GeoDataFrames to avoid conflicts
if 'index_left' in peak_metrics_network.columns or 'index_right' in peak_metrics_network.columns:
    peak_metrics_network = peak_metrics_network.rename(columns={'index_left': 'pm_index_left', 'index_right': 'pm_index_right'})

if 'index_left' in vulnerability.columns or 'index_right' in vulnerability.columns:
    vulnerability = vulnerability.rename(columns={'index_left': 'vul_index_left', 'index_right': 'vul_index_right'})

# Perform the spatial join
final_segments = gpd.sjoin(
    peak_metrics_network,  
    vulnerability,         
    how="left",            
    predicate="intersects")

# Convert back to CRS
final_segments = final_segments.to_crs("EPSG: 4326")
final_segments = final_segments.sort_values(by='Social_Vulnerability_Index', ascending=False)
final_segments.head()
pm_index_right stop_id bus_arrivals frequency headway stop_name stop_lat stop_lon location_type parent_station ... name10 n_veryhigh year hsi_score hei_score hvi_score objectid Shape__Area Shape__Length Social_Vulnerability_Index
16654 4767.0 15973.0 46.0 15.333333 3.913043 Broad St & Wyoming Av 40.024676 -75.147927 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0
16443 4806.0 16894.0 25.0 8.333333 7.200000 Germantown Av & Windrim Av 40.022719 -75.158881 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0
27967 4828.0 31460.0 25.0 8.333333 7.200000 Germantown Av & Berkley St 40.023818 -75.159409 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0
16407 4823.0 16892.0 25.0 8.333333 7.200000 Germantown Av & Dennie St 40.020448 -75.156774 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0
27968 4828.0 31460.0 25.0 8.333333 7.200000 Germantown Av & Berkley St 40.023818 -75.159409 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0

5 rows × 49 columns

Vulnerability on the bus network

We have generated a map to visualize transit rider vulnerability across the bus network, integrating the Social Vulnerability Index (SVI) to highlight areas of concern. The analysis reveals that West Philadelphia and North Philadelphia exhibit the highest levels of social vulnerability.

fig, ax = plt.subplots(figsize=(10, 8))
final_segments.plot(
    ax=ax,
    column="Social_Vulnerability_Index",  
    cmap="plasma",        
    legend=True,           
    markersize=1         
)

plt.title("Vulnerability by Street Segment", fontsize=16)
plt.xlabel("Longitude")
plt.ylabel("Latitude")
Text(118.3577266549966, 0.5, 'Latitude')

Criteria for Critical Bus Network Segments Improvements

To prioritize bus transit solutions effectively, we established the following criteria for determining high-priority areas:

Transit Vulnerability Index: Areas must have a Transit Vulnerability Index (TVI) score of 4 or higher, highlighting significant levels of social vulnerability.

Headway: Routes with headways under 2 minutes are considered, ensuring focus on high-frequency services where improvements can maximize impact.

Ridership: Only routes within the upper quartile of ridership are included, reflecting areas of high demand and usage.

These criteria allow us to target interventions where they are most needed, addressing equity, efficiency, and demand in a data-driven manner.

upper_quartile = final_segments["Ridership"].quantile(0.25)

filtered_segments = final_segments[
    (final_segments["Social_Vulnerability_Index"] >= 3) &  
    (final_segments["headway"] < 5) &                    
    (final_segments["Ridership"] >= upper_quartile)]

filtered_segments.head()
pm_index_right stop_id bus_arrivals frequency headway stop_name stop_lat stop_lon location_type parent_station ... name10 n_veryhigh year hsi_score hei_score hvi_score objectid Shape__Area Shape__Length Social_Vulnerability_Index
16654 4767.0 15973.0 46.0 15.333333 3.913043 Broad St & Wyoming Av 40.024676 -75.147927 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0
16389 4767.0 15973.0 46.0 15.333333 3.913043 Broad St & Wyoming Av 40.024676 -75.147927 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0
16388 4767.0 15973.0 46.0 15.333333 3.913043 Broad St & Wyoming Av 40.024676 -75.147927 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0
16386 4767.0 15973.0 46.0 15.333333 3.913043 Broad St & Wyoming Av 40.024676 -75.147927 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0
16653 4767.0 15973.0 46.0 15.333333 3.913043 Broad St & Wyoming Av 40.024676 -75.147927 NaN NaN ... 280.0 1.0 2023.0 6.741606 0.974944 4.672679 344.0 1.226439e+06 4682.828235 6.0

5 rows × 49 columns

Visualising the most important segments for improvements:

The segments selected based on the criteria outlined above are concentrated along major streets in Philadelphia, including Broad Street, Market Street, Kensington Avenue, and Roosevelt Boulevard. High ridership on these segments likely reflects passenger transfers from nearby subway stations.

from shapely.geometry import Point

# Re-project to a projected CRS for accurate centroid calculation
projected_segments = filtered_segments.to_crs(epsg=3857)

# Calculate the mean centroid coordinates in the projected CRS
mean_y = projected_segments.geometry.centroid.y.mean()
mean_x = projected_segments.geometry.centroid.x.mean()

# Convert the mean centroid coordinates back to WGS84 for Folium
mean_point = gpd.GeoSeries([Point(mean_x, mean_y)], crs=3857).to_crs(epsg=4326)
mean_lat, mean_lon = mean_point.geometry[0].y, mean_point.geometry[0].x

# Initialize a Folium map centered around the data
m = folium.Map(
    location=[mean_lat, mean_lon],
    zoom_start=11,
    tiles='CartoDB Positron')

# choropleth map for TVI
folium.Choropleth(
    geo_data=vulnerability,
    name="Social Vulnerability Score",
    data=vulnerability,
    columns=["GEOID", "Social_Vulnerability_Index"],
    key_on="feature.properties.GEOID", 
    fill_color="YlOrRd",
    fill_opacity=0.3,
    line_opacity=0.1,
    legend_name="Social Vulnerability Score",
).add_to(m)

folium.GeoJson(
    bus_network_philadelphia,
    style_function=lambda x: {"color": "grey", "weight": 0.5, "opacity": 0.4},
    name="Bus Network (Philadelphia)"
).add_to(m)

folium.GeoJson(
    filtered_segments,
    style_function=lambda x: {"color": "blue", "weight": 3, "opacity": 1},
    name="Filtered Segments"
).add_to(m)

folium.LayerControl().add_to(m)

m
Make this Notebook Trusted to load map: File -> Trust Notebook

The table below showcases the top 10 bus segments ranked by ridership. These segments represent the busiest parts of the network, where high passenger volumes underscore their critical role in Philadelphia’s transit system. Prioritizing improvements in these areas is essential to enhance service reliability and meet the needs of the city’s most active transit users. By focusing on these high-demand corridors, transit agencies can address existing inefficiencies and better align services with the needs of vulnerable communities.

# Sort the GeoDataFrame by the 'headway' column in ascending order
filtered_segments = filtered_segments.sort_values(by='headway', ascending=True)

# Group by 'stop_name' and calculate aggregate values for the selected columns
final_streets = (
    filtered_segments.groupby("stop_name")[["headway", "frequency", "Ridership", "Social_Vulnerability_Index"]]
    .mean() 
    .reset_index())

final_streets.rename(columns={"Social_Vulnerability_Index": "Transit_Rider_Vulnerability_Index"}, inplace=True)
final_streets = final_streets.sort_values(by='Ridership', ascending=False)
final_streets.head(10)
stop_name headway frequency Ridership Transit_Rider_Vulnerability_Index
10 Arrott Transportation Center 2.903226 20.666667 1550.0 3.000000
1 23rd St & Venango St Loop 1.855670 32.333333 1503.0 4.000000
26 Broad St & Olney Av - FS 3.750000 16.000000 825.0 3.750000
63 Roosevelt Blvd & Broad St - FS 3.913043 15.333333 666.0 3.000000
30 Broad St & Tabor Rd 3.461538 17.333333 665.0 3.285714
47 Market St & 40th St 4.090909 14.666667 575.0 3.000000
2 33rd St & Dauphin St Loop 4.864865 12.333333 536.0 4.000000
35 Frankford Av & Margaret St 3.529412 17.000000 492.0 3.000000
73 Woodland Av & 50th St 3.600000 16.666667 481.0 5.000000
18 Broad St & Allegheny Av 3.471429 17.333333 415.0 4.000000